<?php
/**
 * model.class
 *
 * @version:	0.1a
 * @package:	global
 * @author:		alex_ez
 * @created:	28.01.2009 02:59:55
 *
 * @copyright:	(c) 2009 alex <qfox@ya.ru> yaroshevich aka alex_ez. All rights reserved.
 */

class model_exception extends exception
{
	
}

abstract class basic_model
{
	/**
	 * таблица
	 *
	 */
	protected $table = null;

	/**
	 * таблица связей
	 * deprecated
	 */
	protected $linked_table = null;

	/**
	 * поле, по которому таблица связей линкуется с id в основной
	 * deprecated
	 */
	protected $linked_field = null;

	/**
	 * линк к объекту базы данных
	 *
	 */
	protected $db = null;
	
	/**
	 * кол-во строк в выборке без учета limit
	 *
	 */
	protected $__found_rows = 0;
	
	/**
	 * fields that will apply to each query to main table except of get(id|tag)-like queries
	 *
	 */
	protected $__fixed_fields = array( );
	
	/**
	 * конструктор
	 * создает базу и выполняет инициализацию. иногда удобнее, чем __construct(){ parent::__construct(); ... }
	 *
	 * @param string $table
	 */
	public function __construct( $table = null )
	{
		$this ->db = db::instance();
		
		$this ->on_start( );
		
		if( !is_null( $table ) )
			$this ->table = $table;
	}
	
	/**
	 * Start event
	 *
	 */
	protected function on_start( ) { }
	
	/**
	 * выборка одной записи по $id
	 *
	 */
	public function get( $id )
	{
		if( is_null( $this ->table ) ) throw new model_exception( "internal error bqz table" );
		
		if (bc::enabled()) bc::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		//if (bc::enabled()) bc::log( .'( '.$id.', '.$cache_time.' )', 'call' );
		$id = (int)($id);
		
		$cache = cache::instance();
		$cache_key = $cache->key(get_class($this), __METHOD__, $id);
		
		$r = $cache->$cache_key;
		if( false!==$r ) {
			if (bc::enabled()) bc::group_end();
			return $r;
		}
		
		//if (bc::enabled()) bc::log( 'recache data' );
		
		$_r = $this ->db ->_row( sprintf( "SELECT * FROM %s WHERE id = %d;", $this ->table, $id ) );
		if( is_null( $_r ) ) return null;
		
		if( $this ->is_linked() ) {
			$_r['linked'] = array();
			$_rp = $this ->db ->_list( sprintf( "SELECT * FROM %s WHERE %s = %d;", $this ->linked_table, $this ->linked_field, $id ), 'id' );
			foreach( $_rp as $k => $v )
			{
				unset($v['id']); unset($v[$this ->linked_field]);
				$_r['linked'][$k] = $v;
			}
		}
		
		$cache->$cache_key = $_r;
		
		if (bc::enabled()) bc::group_end( );
		
		return $_r;
	}
	
	/**
	 * выборка одной записи через поле tag. удобно для страниц, статей, и т.д.
	 *
	 */
	public function by_tag($tag)
	{
		if (bc::enabled()) bc::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		if( !is_string( $tag ) ) throw new model_exception( "wrong parameter in ".__METHOD__ );
		if( is_null( $this ->table ) ) throw new model_exception( "internal error bqz table in ".__METHOD__ );
		
		$cache = cache::instance();
		$cache_key = $cache->key(get_class($this), __METHOD__, $tag);
		
		$r = $cache->$cache_key;
		if( false!==$r ) {
			if (bc::enabled()) bc::group_end();
			return $r;
		}
		
		$r = $this->db->_row( sprintf( "SELECT * FROM %s WHERE tag = '%s';", $this ->table, $this ->db ->escape_string( $tag ) ) );
		$cache->$cache_key = $r;
		
		if (bc::enabled()) bc::group_end();
		
		return $r;
	}

	/**
	 * get by
	 * выборка списка по заданным параметрам.
	 *
	 * @param	array $p	array( 'id' => array( 1,2,5,14 ), ... ) или array( 'tag' => $tag ). собираются через AND
	 * @param	string $o	сортировка. "id+", "id desc", "item_type+,posted-", "item_type asc, posted desc"
	 * @param	array $l	лимит. массив из двух значений. array( 0, 100 ) => limit 0, 100
	 */
	public function by($p = null, $o = null, $l = null)
	{
		$this ->checkup_sql( $p, $o, $l );
		
		$cache = cache::instance();
		$cache_key = $cache->key(get_class($this), __METHOD__, array( $p, $this ->__fixed_fields, $o, $l ));
		
		$r = $cache->$cache_key;
		if( false!==$r ) {
			$this->__found_rows = $r['found_rows'];
			return $r['items'];
		}
		
		if (bc::enabled()) bc::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		$sql = $this ->make_sql( $this ->table, '*', $p, $o, $l );
		
		$_r = $this ->db ->_list( $sql, 'id' );
		$_r = array_filter( $_r );
		$this ->__found_rows = $this ->db ->_value( "SELECT FOUND_ROWS();" );
		
		if( empty( $_r ) ) {
			if (bc::enabled()) bc::log( 'empty result' );
			if (bc::enabled()) bc::group_end( );
			return $_r;
		}
		
		// linked
		if( $this ->is_linked( ) && !empty( $_r ) )
		{
			$ids = array_map( 'intval', array_keys( $_r ) );
			foreach( $ids as $k ) if(isset($_r[$k])) $_r[$k]['linked'] = array( );
			$_rp = $this ->db ->_list( sprintf( "SELECT * FROM %s WHERE %s in ( %s );", $this ->linked_table, $this ->linked_field, join( ', ', $ids ) ) );
			foreach( $_rp as $k => $v )
			{
				$item_id = $v[ $this ->linked_field ];
				unset($v['id']); unset( $v[ $this ->linked_field ] );
				$_r[$item_id]['linked'][$k] = $v;
			}
		}
		
		$r = array( 'items' => $_r, 'found_rows' => $this ->__found_rows );
		$cache->$cache_key = $r;

		if (bc::enabled()) bc::group_end( );

		if ($l[1] === 1)
			return reset($_r);
		
		return $_r;
	}

	/**
	 * get fields by
	 * выборка определенных колонок списка по заданным параметрам.
	 *
	 * @param	array $f	array( 'id' => array( 1,2,5,14 ), ... ) или array( 'tag' => $tag ). собираются через AND
	 * @param	array $p	array( 'id' => array( 1,2,5,14 ), ... ) или array( 'tag' => $tag ). собираются через AND
	 * @param	string $o	сортировка. "id+", "id desc", "item_type+,posted-", "item_type asc, posted desc"
	 * @param	array $l	лимит. массив из двух значений. array( 0, 100 ) => limit 0, 100
	 */
	public function fby($f = '*', $p = null, $o = null, $l = null)
	{
		$this->checkup_sql_fields( $f );
		$this->checkup_sql( $p, $o, $l );

		$cache = cache::instance();
		$cache_key = $cache->key(get_class($this), __METHOD__, array( $p, $this ->__fixed_fields, $o, $l ));

		$r = $cache->$cache_key;
		if( false!==$r ) {
			$this->__found_rows = $r['found_rows'];
			return $r['items'];
		}

		if (bc::enabled()) bc::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );

		$sql = $this->make_sql( $this ->table, $f, $p, $o, $l );

		$_r = $this ->db ->_list( $sql, 'id' );
		$_r = array_filter( $_r );
		$this ->__found_rows = $this ->db ->_value( "SELECT FOUND_ROWS();" );

		if( empty( $_r ) ) {
			if (bc::enabled()) bc::log( 'empty result' );
			if (bc::enabled()) bc::group_end( );
			return $_r;
		}

		$r = array( 'items' => $_r, 'found_rows' => $this ->__found_rows );
		$cache->$cache_key = $r;

		if (bc::enabled()) bc::group_end( );

		if ($l[1] === 1)
			return reset($_r);
		
		return $_r;
	}

	public function gby( $f, $g = null, $p = null, $o = null, $l = null )
	{
		$this->checkup_sql( $p, $o, $l );
		if (is_null($g)) $g = '1';

		$sql = $this->make_sql( $this->table, $f, $p, $o, $l, $g );

		$r = array_filter( $this->db->_list( $sql ) );

		return $r;
	}

	public function ids_by($p = null, $o = null, $l = null)
	{
		$this->checkup_sql( $p, $o, $l );

		$sql = $this ->make_sql( $this ->table, 'id', $p, $o, $l );

		$r = array_filter( $this->db->_list( $sql, null, 'id' ) );
		$this->__found_rows = $this->db->_value( "SELECT FOUND_ROWS();" );

		return $r;
	}

	protected function checkup_sql( &$p = null, &$o = null, &$l = null )
	{
		if ( is_null( $this ->table ) ) throw new model_exception( "internal error bqz table" );
		if ( !is_array( $p ) && ( !is_string( $p ) ) && !is_null( $p ) ) throw new model_exception( "wrong where in ".__METHOD__ );
		if ( is_string( $o ) && !strlen( $o ) ) $o = null;
		if ( !is_null( $o ) && ( !is_string( $o ) || !strlen( $o ) ) ) throw new model_exception( "wrong order in ".__METHOD__ );
		if ( !is_null( $l ) && ( !is_array( $l ) || empty( $l ) ) ) { if( is_numeric( $l ) && intval( $l ) ) $l = array( 0, intval( $l ) ); else throw new model_exception( "wrong limit in ".__METHOD__ ); }
	}

	protected function checkup_sql_fields( &$f )
	{
		if ( is_null( $this ->table ) ) throw new model_exception( "internal error bqz table" );
		if ( is_null($f) && (strpos($f,'id') !== false || strpos($f,'*') !== false) ) throw new model_exception( "wrong fields ".print_r($f,1) );
	}

	public function make_sql( $table = null, $columns = null, $p = null, $o = null, $l = null, $g = null )
	{
		if( empty( $table ) )
			$table = $this ->table;
		
		// columns
		if( is_null( $columns ) )
			$columns = '*';
		elseif( is_array( $columns ) )
			$columns = '`'.join( '`,`', $columns ).'`';
		
		// where
		if( is_array( $p ) ) {
			$pnum = array();
			foreach (array_keys($p) as $pk) {
				if (!is_numeric($pk))
					continue;
				$pnum[] = is_array($p[$pk]) ? $this->db->make_set_list( $p[$pk], 'OR' ) : $p[$pk];
				unset($p[$pk]);
			}
			$_sp = !empty($pnum) ? '(('.join( ') AND (', $pnum ).'))' : '';
			$p = array_merge( $p, $this ->__fixed_fields );
			$p = (empty($p) ? 'TRUE' : $this->db->make_set_list( $p, 'AND' ));
			if ($p != 'TRUE')
				$_sp = ($_sp ? $_sp . ' AND ' . $p : $p);
			if (empty($_sp))
				$_sp = 'TRUE';
		} else {
			$_sp = $this->db->make_set_list( $this ->__fixed_fields, 'AND' );
			//if (bc::enabled()) bc::log( $p, 'in' );
			$p = join( ' AND ', array_filter( array_map( 'trim', explode( 'AND', $p ) ) ) );
			//if (bc::enabled()) bc::log( $p, 'modified' );
			$_sp .= ( $_sp ) ? ' AND ( '.$p.' )' : $p;
		}
		if( !$_sp ) $_sp = 'TRUE';
		$_sp = ' '.$_sp.' ';

		// order
		$order = '';
		if( !is_null( $o ) && is_string( $o ) )
		{
			if( strpos( $o, 'ASC' )||strpos( $o, 'DESC' ) )
			{
				$order = 'ORDER BY '.$o;
			}
			else
			{
				$o = explode( ',', $o );
				$o = array_map( create_function( '$a', '$a=trim($a); if(mb_strtolower(mb_substr($a,-4,4))==" asc") { return mb_substr($a,0,-4)."+"; } elseif(mb_strtolower(mb_substr($a,-5,5))==" desc") return mb_substr($a,0,-5)."-"; return $a;' ), $o );
				$o = array_map( create_function( '$a', '$a=trim($a); if(strpos($a,"`")!==false) return $a; $o=mb_substr($a,-1,1); if($o!="-")$o="+"; $a = trim($a," ".$o); return sprintf(is_numeric($a)?"%d%s":"`%s`%s",$a,$o);' ), $o );
				if (bc::enabled()) bc::log( $o, 'order' );
				$order = 'ORDER BY '.trim(preg_replace( array( '/\\+(,|$)/', '/\\-(,|$)/' ), array( ' ASC, ', ' DESC, ' ), join( ',', $o ) ),' ,');
			}
		}

		// group
		$group = '';
		if( !is_null( $g ) && is_string( $g ) )
			$group = 'GROUP BY '.$g;

		// limit
		$directive = "";
		$limit = "";
		if( !is_null( $l ) )
		{
			$limit = ' LIMIT '.join( ',', $l );
			$directive = 'SQL_CALC_FOUND_ROWS';
		}

		$sql = sprintf( "SELECT %s %s \nFROM %s \nWHERE %s \n%s \n%s \n%s", $directive, $columns, $table, $_sp, $group, $order, $limit );
		return $sql;
	}
	
	public function get_found_rows( )
	{
		return $this ->__found_rows;
	}
	
	public function get_count( )
	{
		$cache = cache::instance();
		$cache->ttl('20M');
		$cache_key = $cache->key(get_class($this),__METHOD__);
		
		if( false !== ($r=$cache->$cache_key) )
			return $r;
		
		$r = $this->db->_value( "SELECT COUNT(id) as `count` FROM {$this->table};" );
		$cache->$cache_key = $r;
		
		return $r;
	}

	public function count( $where = null )
	{
		if(is_null($where))
			return $this->get_count();

		$this->checkup_sql( $where );

		$sql = $this ->make_sql( $this ->table, 'count(id)', $where );
		return $this->db->_value( $sql );
	}
	
	/**
	 * Default add item method
	 *
	 * @param array $data row data
	 * @param int $ignore 0: "INSERT", 1: "INSERT IGNORE", 2: "REPLACE"
	 * @return int New id
	 */
	public function add( $data, $ignore = false )
	{
		if (bc::enabled()) bc::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );

		$data = array_merge( $data, $this ->__fixed_fields );
		$datas = $this ->db ->make_set_list( $data );

		$operator = $ignore?($ignore==1?"INSERT IGNORE":"REPLACE"):"INSERT";

		$sql = "%s INTO %s SET %s;";
		$sql = sprintf( $sql, $operator, $this ->table, $datas );

		$this ->db ->exec( $sql );
		$this ->__found_rows = $this ->db ->affected_rows;
		//if (bc::enabled()) bc::log( array( 'ar' => $this ->db ->affected_rows, 'id' => $this ->db ->insert_id ), 'add'.$ignore );

		$r = $ignore == 2
			? ( $this ->db ->affected_rows != 0 ? $this ->db ->insert_id : false )
			: ( $this ->db ->affected_rows == 1 ? $this ->db ->insert_id : false );
		
		if (bc::enabled()) bc::log( $r, 'result' );
		if (bc::enabled()) bc::group_end( );
		return $r;
	}
	
	public function remove( $id )
	{
		if (bc::enabled()) bc::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		$ids = array_merge( array( 'id' => intval( $id ) ), $this ->__fixed_fields );
		$ids = $this ->db ->make_set_list( $ids, 'AND' );
		
		$sql = "DELETE FROM `%s` WHERE %s;";
		$sql = sprintf( $sql, $this ->table, $ids );
		$this ->db ->exec( $sql );
		$this ->__found_rows = $this ->db ->affected_rows;
		
		$r = $this ->db ->affected_rows;
		if (bc::enabled()) bc::log( $r, 'result' );
		if (bc::enabled()) bc::group_end( );
		return $r;
	}
	
	public function edit( $id, $data )
	{
		if (bc::enabled()) bc::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		$datas = $this ->db ->make_set_list( $data );
		
		$ids = array_merge( array( 'id' => intval( $id ) ), $this ->__fixed_fields );
		$ids = $this ->db ->make_set_list( $ids, ' AND ' );
		
		$sql = "UPDATE `%s` SET %s WHERE %s;";
		$sql = sprintf( $sql, $this ->table, $datas, $ids );
		$this ->db ->exec( $sql );
		$this ->__found_rows = $this ->db ->affected_rows;
		
		$r = $this ->db ->affected_rows;
		if (bc::enabled()) bc::log( $r, 'result' );
		if (bc::enabled()) bc::group_end( );
		return $r;
	}
	
	
	
	
	// linked
	public function is_linked( $exception_on_fail = false )
	{
		if( !$this ->linked_on
		 || is_null( $this ->linked_table ) || !isset( $this ->linked_table[1] )
		 || is_null( $this ->linked_field ) || !isset( $this ->linked_field[1] ) )
		{
			if( !$exception_on_fail ) return false;
			throw new model_exception( sprintf( __('Model "%s" have no linked table.field pair'), get_class( $this ) ) );
		}
		
		return true;
	}
	
	protected $linked_on = true;
	public function dont_load_linked( )
	{
		$this ->linked_on = false;
	}
	public function do_load_linked( )
	{
		$this ->linked_on = true;
	}

	
	public function add_link( $data, $id = null )
	{
		$this ->is_linked( 1 );
		
		if( !is_null( $id ) ) // add id to data
			$data[ $this ->linked_field ] = $id;
		
		$data = $this ->db ->make_set_list( $data );
		
		$sql = "REPLACE INTO %s SET %s;";
		$sql = sprintf( $sql, $this ->linked_table, $data );
		
		$this ->db ->exec( $sql );
		
		return ( $this ->db ->affected_rows != 0 ? $this ->db ->insert_id : false );
	}
	
	// todo: rewrite
	public function add_links( $datas, $id = null )
	{
		$this ->is_linked( 1 );
		
		if( !isset( $this ->linked_table ) || !isset( $this ->linked_table[1] ) ) return null;
		$_r = array( );
		foreach( $datas as $k => $data )
		{
			if( !is_null( $id ) ) // add item_id to each data
				$data[ $this ->linked_field ] = $id;
			$_r[$k] = $this ->add_link( $data );
		}
		
		return $_r;
	}
	
	public function remove_link( $ids )
	{
		$this ->is_linked( 1 );
		
		$ids = $this ->db ->make_set_list( $ids, 'AND' );
		
		$sql = "DELETE FROM `%s` WHERE %s;";
		$sql = sprintf( $sql, $this ->linked_table, $ids );
		$this ->db ->exec( $sql );
		
		return $this ->db ->affected_rows;
	}
	
	public function remove_links( $linked_id )
	{
		$this ->is_linked( 1 );
		
		return $this ->remove_links_by_keys( array( $this ->linked_field => $linked_id ) );
	}
	
/*
	public function remove_links_to_item( $item_type, $item_id )
	{
		if( !isset( $this ->linked_table ) || !isset( $this ->linked_table[1] ) ) return null;
		return $this ->remove_links_by_keys( array( 'item_type' => $item_type, 'item_id' => $item_id ) );
	}
*/
	
	public function remove_links_by_keys( $keys )
	{
		$this ->is_linked( 1 );
		
		$keys = $this ->db ->make_set_list( $keys, 'AND' );
		
		if (bc::enabled()) bc::log($keys);
		
		$sql = "DELETE FROM %s WHERE %s;";
		$sql = sprintf( $sql, $this ->linked_table, $keys );
		$this ->db ->exec( $sql );
		return $this ->db ->affected_rows;
	}
	
	
	
	/**
	 * states
	 */
	public function set_state( $id, $state )
	{
		$this ->edit( $id, array( 'state' => $state ) );
	}
	
	public function get_state( $id )
	{
		$l = $this ->get( $id );
		return $l['state'];
	}

	/**
	 * Service
	 */

	/**
	 * Gets model name
	 * @return string
	 */
	public function get_name( )
	{
		return str_replace( '_', '-', get_class( $this ) );
	}

	/**
	 * Gets db table of model
	 * @return string
	 */
	public function get_table( )
	{
		return $this ->table;
	}

}

